﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.IO;
using System.Xml;

namespace BawangMemo.BackendModels
{
    public class CourseRepository
    {
        public const int DEfAULT_LIST_SIZE = 150;

        public static void CreateCourse(string courseName, string bookPath, WordOrder order, int bucketSize)
        {
            CreateCourse(courseName, bookPath, order, bucketSize, false);
        }

        /// <summary>
        /// Creates a course from a book.
        /// </summary>
        /// <param name="courseName">the course name</param>
        /// <param name="bookPath">the path of the book</param>
        /// <param name="order">the order of the words in the course</param>
        /// <param name="bucketSize">the size of each section (bucket) of the course</param>
        /// <exception cref="ApplicationException">course creation fails.</exception>
        public static void CreateCourse(string courseName, string bookPath, WordOrder order, int bucketSize, bool isLoadTag)
        {
            // check book existence
            if (!File.Exists(bookPath))
            {
                throw new ApplicationException(String.Format("Book [{0}] does not exist.", bookPath));
            }

            // check course existence
            if (hasCourseName(courseName))
            {
                throw new ApplicationException(String.Format("Course [{0}] already exists.", courseName));
            }

            // build word list
            int totalWordCount = BuildWordList(courseName, bookPath, order);

            // create course wrapper
            CourseWrapper cw = new CourseWrapper(courseName, bucketSize, totalWordCount);
            SaveCourse(cw);
            ListRepository.CreateCourseListTable(cw, DEfAULT_LIST_SIZE);
            TagRepository.CreateCourseTagTable(cw);
            RecordRepository.CreateRecordTable(cw);

            // add predefined tags
            BookInfo bookInfo = BookRepository.GetBookInfo(Path.GetFileNameWithoutExtension(bookPath));
            if (isLoadTag &&
                bookInfo != null &&
                File.Exists(bookInfo.TagCategoryPath) &&
                File.Exists(bookInfo.TagDataPath))
            {
                LoadPredefinedTags(cw, bookInfo);
            }

            // modify course.lst
            var elem = new XElement(Constants.COURSE_LIST_SUB_ELEMENT);
            elem.SetAttributeValue("name", courseName);
            var doc = XDocument.Load(Constants.CourseListFileName);
            doc.Element(Constants.COURSE_LIST_ROOT_ELEMENT).Add(elem);

            Helper.SaveXML(doc, Constants.CourseListFileName);
        }

        private static void LoadPredefinedTags(CourseWrapper cw, BookInfo bookInfo)
        {
            string s;
            IDictionary<string, string> cat =  new Dictionary<string, string>();
            using (StreamReader sr = new StreamReader(bookInfo.TagCategoryPath))
            {
                while ((s = sr.ReadLine()) != null)
                {
                    string[] arr = s.Split(new char[] { ' ', '\t' }, 2);
                    cat.Add(arr[0], arr[1]);
                }
            }
            var insertList = new List<KeyValuePair<WordWrapper, string>>();
            var wordSet = new HashSet<string>();
            using (StreamReader sr = new StreamReader (bookInfo.TagDataPath))
            {
                while ((s = sr.ReadLine()) != null)
                {
                    string[] arr = s.Split(new char[] { ' ', '\t' });
                    if (wordSet.Contains(arr[0]))
                        continue;
                    wordSet.Add(arr[0]);
                    List<WordWrapper> wwl = SpellToWordWrapper(cw, arr[0]);
                    if (wwl != null)
                    {
                        foreach (WordWrapper ww in wwl)
                        {
                            HashSet<string> addedSet = new HashSet<string>();
                            for (int i = 1; i < arr.Length; i++)
                            {
                                if (cat.ContainsKey(arr[i]) && !addedSet.Contains(arr[i]))
                                {
                                    addedSet.Add(arr[i]);
                                    insertList.Add(new KeyValuePair<WordWrapper, string>(ww, cat[arr[i]]));
                                }
                            }
                        }
                    }
                }
            }
            //int count = 0;
            //foreach (KeyValuePair<WordWrapper, string> kvp in insertList)
            //{
            //    TagRepository.AddTag(kvp.Key, kvp.Value);
            //    Console.WriteLine(++count);
            //}

            TagRepository.BulkInsert(cw, insertList);

            Console.WriteLine(insertList.Count + " inserted!");
        }

        private static List<WordWrapper> SpellToWordWrapper(CourseWrapper cw, string spell)
        {
            return cw.WordDict.ContainsKey(spell) ? cw.WordDict[spell] : null;
            //var list = cw.WordList.Where(w => w.Spell.ToLower().Equals(spell.ToLower())).ToList();
            //return list.Count > 0 ? list[0] : null;
        }

        /// <summary>
        /// Creates a course from a list of words
        /// </summary>
        /// <param name="courseName">the course name.</param>
        /// <param name="wordList">the list of words.</param>
        /// <param name="order">the word order.</param>
        /// <param name="bucketSize">the bucket size of the course.</param>
        public static void CreateCourse(string courseName, List<WordWrapper> wordList, WordOrder order, int bucketSize)
        {
                        // check course existence
            if (hasCourseName(courseName))
            {
                throw new ApplicationException(String.Format("Course [{0}] already exists.", courseName));
            }

            // build word list
            int totalWordCount = BuildWordList(courseName, wordList, order);

            // create course wrapper
            CourseWrapper cw = new CourseWrapper(courseName, bucketSize, totalWordCount);
            SaveCourse(cw);
            ListRepository.CreateCourseListTable(cw, DEfAULT_LIST_SIZE);
            TagRepository.CreateCourseTagTable(cw);

            // modify course.lst
            var elem = new XElement(Constants.COURSE_LIST_SUB_ELEMENT);
            elem.SetAttributeValue("name", courseName);
            var doc = XDocument.Load(Constants.CourseListFileName);
            doc.Element(Constants.COURSE_LIST_ROOT_ELEMENT).Add(elem);

            Helper.SaveXML(doc, Constants.CourseListFileName);
        }

        public static void DeleteCourse(string courseName)
        {
            if (!hasCourseName(courseName))
            {
                return;
            }
            
            // delete from course.lst
            var doc = XDocument.Load(Constants.CourseListFileName);
            foreach (XElement elem in doc.Descendants(Constants.COURSE_LIST_SUB_ELEMENT))
            {
                if (elem.Attribute("name").Value.Equals(courseName))
                {
                    elem.Remove();
                    break;
                }
            }
            Helper.SaveXML(doc, Constants.CourseListFileName);

            // delete course related files
            if (File.Exists(GetCourseInfoPath(courseName)))
            {
                File.Delete(GetCourseInfoPath(courseName));
            }
            if (File.Exists(GetWordInfoPath(courseName)))
            {
                File.Delete(GetWordInfoPath(courseName));
            }
            if (File.Exists(GetWordListPath(courseName)))
            {
                File.Delete(GetWordListPath(courseName));
            }

            // delete from the tag database
            TagRepository.DeleteCourseTagTable(courseName);
        }

        public static CourseWrapper LoadCourse(string courseName)
        {
            return new CourseWrapper(LoadCourseInfo(courseName));
        }

        public static CourseInfo LoadCourseInfo(string courseName)
        {
            return (CourseInfo) DataAccess.FileToObject(GetCourseInfoPath(courseName));
        }

        public static void SaveCourse(CourseWrapper course)
        {
            SaveCourseInfo(course.CourseInfo, course.CourseInfoPath);
        }

        public static void SaveCourseInfo(CourseInfo courseInfo, string courseInfoPath)
        {
            DataAccess.ObjectToFile(courseInfo, courseInfoPath);
        }

        private static string GetCourseInfoPath(string courseName)
        {
            return Constants.CourseDirectory + courseName + Constants.EXT_COURSE_INFO;
        }

        private static string GetWordInfoPath(string courseName)
        {
            return Constants.CourseDirectory + courseName + Constants.EXT_WORD_INFO;
        }
        private static string GetWordListPath(string courseName)
        {
            return Constants.CourseDirectory + courseName + Constants.EXT_WORD_LIST;
        }

        public static bool hasCourseName(string courseName)
        {
            return (from elem in XDocument.Load(Constants.CourseListFileName).Descendants("course")
                    where (elem.Attribute("name") != null && elem.Attribute("name").Value.Equals(courseName))
                    select elem).Count() > 0;
        }

        public static List<CourseInfo> ListCourses()
        {
            XDocument doc = XDocument.Load(Constants.CourseListFileName);
            return (from elem in doc.Descendants("course")
                    select LoadCourseInfo(elem.Attribute("name").Value)).ToList();
        }

        public static Dictionary<string, double> GetAllProgress()
        {
            var result = new Dictionary<string, double>();
            foreach (CourseInfo ci in ListCourses())
            {
                CourseWrapper cw = CourseRepository.LoadCourse(ci.CourseName);
                double progress = (cw.CurrentWordIndex + 1.0) / cw.TotalWordCount;//mark
                //double progress = cw.Progress;
                result.Add(ci.CourseName, progress);
            }
            return result;
        }

        #region Private Helpers

        private static int BuildWordList(string courseName, string bookPath, WordOrder order)
        {
            // creates course data files
            string path = Constants.CourseDirectory + courseName + Constants.EXT_WORD_LIST;
            //if (File.Exists(path))
            //{
            //    throw new ApplicationException(String.Format("Course [{0}] already exists.", courseName));
            //}

            // creates new xml document
            XDocument target = new XDocument();
            XElement root = new XElement(Constants.WORD_LIST_ROOT_ELEMENT);
            target.Add(root);

            // loads <word> elements from book.
            var oldList = WordRepository.LoadWordElementsFromBook(bookPath);
            var indexOrder = Helper.GenerateOrder(order, oldList.Count);
            for (int i = 0; i < oldList.Count; i++)
            {
                var wordElement = oldList[indexOrder[i]];
                wordElement.SetAttributeValue("id", i + 1);
                root.Add(wordElement);
            }

            // writes to file
            using (XmlWriter xw = XmlWriter.Create(path))
            {
                target.WriteTo(xw);
                xw.Flush();
                xw.Close();
            }

            // build word info list
            path = Constants.CourseDirectory + courseName + Constants.EXT_WORD_INFO;

            List<WordInfo> list = new List<WordInfo>();
            for (int i = 0; i < indexOrder.Length; i++)
                list.Add(new WordInfo(i + 1));
            WordRepository.SaveAllWordInfo(list, path);

            return list.Count;
        }

        private static int BuildWordList(string courseName, List<WordWrapper> wwList, WordOrder order)
        {
            // creates course data files
            string path = Constants.CourseDirectory + courseName + Constants.EXT_WORD_LIST;

            // creates new xml document
            XDocument target = new XDocument();
            XElement root = new XElement(Constants.WORD_LIST_ROOT_ELEMENT);
            target.Add(root);

            // loads <word> elements from book.
            var indexOrder = Helper.GenerateOrder(order, wwList.Count);
            
            for (int i = 0; i < wwList.Count; i++)
            {
                WordWrapper ww = wwList[indexOrder[i]];
                CourseWrapper cw = ww.Course;
                //XElement wordElement = WordRepository.LoadWordElementFromCourse(cw.WordListPath, wwList[i].Id);
                XElement wordElement = WordRepository.WordCardToWordElement(cw.GetWordCard(ww.Id));
                wordElement.SetAttributeValue("id", i + 1);
                root.Add(wordElement);
            }

            // writes to file
            using (XmlWriter xw = XmlWriter.Create(path))
            {
                target.WriteTo(xw);
                xw.Flush();
                xw.Close();
            }

            // build word info list
            path = Constants.CourseDirectory + courseName + Constants.EXT_WORD_INFO;

            List<WordInfo> list = new List<WordInfo>();
            for (int i = 0; i < indexOrder.Length; i++)
                list.Add(new WordInfo(i + 1));
            WordRepository.SaveAllWordInfo(list, path);

            return list.Count;
        }

        #endregion

        public static CourseWrapper LoadLastCourse()
        {
            CourseInfo latest = null;
            foreach (CourseInfo ci in ListCourses())
            {
                if (latest == null ||
                    IsLaterThan(ci, latest))
                    latest = ci;
            }
            return latest == null ? null : new CourseWrapper(latest);
        }

        private static bool IsLaterThan(CourseInfo ci, CourseInfo latest)
        {
            return GetModifiedTime(ci).CompareTo(GetModifiedTime(latest)) > 0;
        }

        private static DateTime GetModifiedTime(CourseInfo courseInfo)
        {
            string path = GetCourseInfoPath(courseInfo.CourseName);
            return File.GetLastWriteTime(path);
        }

    }
}
